← Writeups

Exploiting clickjacking vulnerability to trigger DOM-based XSS

Cadena clickjacking + DOM XSS: el nombre de feedback se inyecta vía innerHTML, se pre-rellena desde la URL y ClickBandit orquesta el clic que dispara print().

Target Overview & Vulnerability Summary

El lab contiene un XSS disparado por un clic. La funcionalidad de envío de feedback refleja el campo name dentro de feedbackResult mediante innerHTML (sink inseguro). El ataque encadena: (1) pre-rellenar el formulario de feedback con un payload XSS vía parámetros URL, (2) enviarlo, y (3) usar clickjacking para que la víctima haga clic en el botón que dispara el submit — la combinación ejecuta print().

Campo Valor
Severidad Alta (ejecución de JavaScript en el contexto de la víctima vía interacción engañosa)
Endpoint vulnerable /feedback (campo name → sink innerHTML)
Root cause Sink innerHTML con datos no sanitizados + página embebible (ausencia de anti-framing)

Technical Breakdown & Exploitation Flow

1. El sink innerHTML

El formulario de feedback:

<form id="feedbackForm" action="/feedback/submit" method="POST" enctype="application/x-www-form-urlencoded" personal="true">
    <input required type="hidden" name="csrf" value="YBZbpn20pVybpKdbSivPBuMawNzmw6IL">
    <label>Name:</label>
    <input required type="text" name="name">
    <label>Email:</label>
    <input required type="email" name="email">
    <label>Subject:</label>
    <input required type="text" name="subject">
    <label>Message:</label>
    <textarea required rows="12" cols="300" name="message"></textarea>
    <button class="button" type="submit">Submit feedback</button>
    <span id="feedbackResult"></span>
</form>

Y el script submitFeedback.js:

document.getElementById("feedbackForm").addEventListener("submit", function(e) {
    submitFeedback(this.getAttribute("method"), this.getAttribute("action"), this.getAttribute("enctype"), this.getAttribute("personal"), new FormData(this));
    e.preventDefault();
});

function submitFeedback(method, path, encoding, personal, data) {
    var XHR = new XMLHttpRequest();
    XHR.open(method, path);
    if (personal) {
        XHR.addEventListener("load", displayFeedbackMessage(data.get('name')));
    } else {
        XHR.addEventListener("load", displayFeedbackMessage());
    }
    if (encoding === "multipart/form-data") {
        XHR.send(data)
    } else {
        var params = new URLSearchParams(data);
        XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        XHR.send(params.toString())
    }
}

function displayFeedbackMessage(name) {
    return function() {
        var feedbackResult = document.getElementById("feedbackResult");
        if (this.status === 200) {
            feedbackResult.innerHTML = "Thank you for submitting feedback" + (name ? ", " + name : "") + "!";
            feedbackForm.reset();
        } else {
            feedbackResult.innerHTML =  "Failed to submit feedback: " + this.responseText
        }
    }
}

El valor del campo name se concatena en innerHTML sin validación, sanitización ni escapado:

feedbackResult.innerHTML = "Thank you for submitting feedback" + (name ? ", " + name : "") + "!";

2. Payload XSS

El payload se refleja y ejecuta tras el submit:

<img src=errorpls onerror=alert(document.domain)>

3. Prefill + clickjacking

El formulario se pre-rellena desde los parámetros de la URL (como en el lab de prefill de formulario):

/feedback?name=<img src=errorpls onerror=print()>&email=attacker@evil.com&subject=subject&message=message

Con ClickBandit se genera el HTML de clickjacking que reproduce el clic sobre el botón "Submit feedback": la víctima ve un señuelo, hace clic, el formulario pre-rellenado se envía, y la respuesta (con el name inyectado) se vuelca en innerHTML, ejecutando print() en el contexto de la página.

Cadena completa:

  1. name contiene <img src=errorpls onerror=print()> — pre-cargado por URL.
  2. La víctima hace clic en el señuelo del iframe transparente.
  3. El submit envía el formulario; la respuesta del servidor rellena feedbackResult.innerHTML con el payload.
  4. El <img> con src inválido dispara onerror → print().

Root Cause Analysis

Se combinan tres fallos:

  1. Sink innerHTML sin saneamiento: el campo name del feedback se refleja como HTML en el DOM del cliente.
  2. Prefill desde URL: los campos del formulario se rellenan desde parámetros, permitiendo inyectar el payload sin interacción previa del atacante sobre la página.
  3. Página embebible: sin X-Frame-Options ni CSP frame-ancestors, el iframe transparente permite orquestar el clic que dispara el submit.

El CSRF token es irrelevante aquí: la request la genera el navegador de la víctima sobre el origen correcto.

Secure Remediation

  • Sanitizar la salida del feedback: usar textContent para el mensaje o escapar el valor de name antes de innerHTML:
feedbackResult.textContent = "Thank you for submitting feedback" + (name ? ", " + name : "") + "!";
  • Bloquear el framing de la página:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
  • No pre-rellenar campos de formulario desde parámetros URL en flujos que mutan estado, o validar el contenido contra una lista blanca.
  • Añadir CSP script-src estricto para que handlers inline (onerror) no ejecuten incluso si un sink queda expuesto.